Skip to content

Commit 25ea081

Browse files
Merge pull request #1 from chaoss/muggle
Signed-off-by: Sourabh Saraswat 191939 <[email protected]>
2 parents d322178 + 988fce3 commit 25ea081

File tree

17 files changed

+946
-89
lines changed

17 files changed

+946
-89
lines changed

CONTRIBUTING.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Contributing to GrimoireLab Sorting Hat
2+
3+
These are some general guidelines and information related to how we contribute to
4+
GrimoireLab. You can read about it from the [CONTRIBUTING.md](https://github.com/chaoss/grimoirelab/blob/master/CONTRIBUTING.md).
5+
6+
## Changelog Entries
7+
8+
Some of your contributions will require a changelog entry which explains
9+
the motivation of the change. These entries would be included in the
10+
release notes to explain users and developers about the new features
11+
or bugs fixed in the software. This is an example of changelog entry:
12+
13+
```
14+
title: 'Fix bug casting spells on magicians'
15+
category: fixed
16+
author: John Smith <[email protected]>
17+
issue: 666
18+
notes: >
19+
The bug was making impossible to cast a spell on
20+
a magician.
21+
```
22+
23+
Changelog entries will be written to explain *what* was changed and *why*,
24+
not *how*. Take into account not everybody is a developer and they are
25+
meant to reach a wider audience.
26+
27+
### What warrants a changelog entry
28+
29+
Changelog entries are **required** for:
30+
- Code changes that directly affects the GrimoireLab users.
31+
- Bug fixes.
32+
- New updates.
33+
- Performance improvements.
34+
35+
Changelog entries are **not required** for
36+
- Docs-only (e.g., README.md) changes.
37+
- Developer-facing change (e.g., test suite changes).
38+
- Code refactoring.
39+
40+
### Writing changelog entries
41+
42+
These changelog entries should be written to the `releases/unreleased/`
43+
directory. The file is expected to be a [YAML](https://yaml.org/) file
44+
in the following format:
45+
46+
```
47+
title: 'Fix bug casting spells on magicians'
48+
category: fixed
49+
author: John Smith <[email protected]>
50+
issue: 666
51+
notes: >
52+
The bug was making impossible to cast a spell on
53+
a magician.
54+
```
55+
56+
The `title` field has the name of the change. This is a mandatory field.
57+
58+
The `category` field maps the category of the change, valid options are:
59+
added, fixed, changed, deprecated, removed, security, performance, other.
60+
This field is mandatory.
61+
62+
The `author` key (format: `Your Name <[email protected]>`) is used to
63+
give attribution to community contributors. This is an optional field but
64+
the Community contributors are encouraged to add their names.
65+
66+
The `issue` value is a reference to the issue, if any, that is targeted
67+
with this change. This is an optional field.
68+
69+
The `notes` field should have a description explaining the changes in the
70+
code. Remember you can write blocks of text using the `>` character at the
71+
beginning of each block. See the above given example.
72+
73+
Contributors can use the interactive [changelog](https://github.com/Bitergia/release-tools#changelog)
74+
tool for this purpose which generates the changelog entry file automatically.
75+
76+
### Tips for writing good changelog entries
77+
78+
A good changelog entry should be descriptive and concise. It should explain
79+
the change to a reader who has *zero context* about the change. If you have
80+
trouble making it both concise and descriptive, err on the side of descriptive.
81+
82+
Use your best judgment and try to put yourself in the mindset of someone
83+
reading the compiled changelog. Does this entry add value? Does it offer
84+
context about *what* and *why* the change was made?
85+
86+
#### Examples
87+
88+
- **Bad:** Go to a project order.
89+
- **Good:** Show a user’s starred projects at the top of the “Go to project” dropdown.
90+
91+
The first example provides no context of where the change was made, or why, or
92+
how it benefits the user.
93+
94+
- **Bad:** Copy (some text) to clipboard.
95+
- **Good:** Update the “Copy to clipboard” tooltip to indicate what’s being copied.
96+
97+
Again, the first example is too vague and provides no context.
98+
99+
- **Bad:** Fixes and Improves CSS and HTML problems in mini pipeline graph and builds dropdown.
100+
- **Good:** Fix tooltips and hover states in mini pipeline graph and builds dropdown.
101+
102+
The first example is too focused on implementation details. The user doesn’t care
103+
that we changed CSS and HTML, they care about the result of those changes.
104+
105+
- **Bad:** Strip out `nil`s in the Array of Commit objects returned from `find_commits_by_message_with_elastic`
106+
- **Good:** Fix 500 errors caused by Elasticsearch results referencing garbage-collected commits
107+
108+
The first example focuses on *how* we fixed something, not on *what* it fixes.
109+
The rewritten version clearly describes the *end benefit* to the user
110+
(fewer 500 errors), and *when* (searching commits with Elasticsearch).

docs/database.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## Database
2+
3+
The conceptual schema of the SortingHat database is shown below. Individuals are the first-class citizens. They have a profile, which summarizes the member data, and can be linked to more than one identity and organization, which are automatically extracted from the software development tools of your project. Note that organizations or identities can be easily excluded from SortingHat by registering their names/emails/usernames to a _matching blacklist_. The filter associated to the blacklist is executed every time an identity is inserted to the database or modified.
4+
5+
![sh-database-diagram](sh-database-diagram.svg)

docs/sh-database-diagram.svg

Lines changed: 3 additions & 0 deletions
Loading

sortinghat/core/jobs.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ def recommend_matches(ctx, source_uuids, target_uuids, criteria, verbose=False):
187187
:returns: a dictionary with which individuals are recommended to be
188188
merged to which individual or which identities.
189189
"""
190+
check_criteria(criteria)
191+
190192
job = rq.get_current_job()
191193

192194
logger.info(f"Running job {job.id} 'recommend matches'; criteria='{criteria}'; ...")
@@ -329,6 +331,8 @@ def _group_recommendations(recs):
329331
groups.append(g_uuids)
330332
return groups
331333

334+
check_criteria(criteria)
335+
332336
job = rq.get_current_job()
333337

334338
logger.info(f"Running job {job.id} 'unify'; criteria='{criteria}'; ...")
@@ -475,3 +479,16 @@ def _iter_split(iterator, size=None):
475479
yield itertools.chain([peek], slice_iter)
476480
except StopIteration:
477481
return
482+
483+
484+
def check_criteria(criteria):
485+
""" Check if all given criteria are valid.
486+
487+
Raises an error if a criterion is not in the valid criteria list
488+
(`email`, `name` and/or `username`).
489+
490+
:param criteria: list of criteria to check
491+
"""
492+
valid_criteria = ['name', 'email', 'username']
493+
if any(criterion not in valid_criteria for criterion in criteria):
494+
raise ValueError(f"Invalid criteria {criteria}. Valid values are: {valid_criteria}")

sortinghat/core/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ class Individual(EntityBase):
176176

177177
class Meta:
178178
db_table = 'individuals'
179-
ordering = ('last_modified', 'created_at',)
179+
ordering = ('last_modified', 'created_at', 'profile__name',)
180180

181181
def __str__(self):
182182
return self.mk

tests/test_schema.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,6 +2538,43 @@ def test_order_by_created_at(self):
25382538
indv = individuals[2]
25392539
self.assertEqual(indv['mk'], indv1.mk)
25402540

2541+
def test_order_by_profile_name(self):
2542+
"""Check whether it returns the individuals ordered by their name"""
2543+
2544+
indv1 = Individual.objects.create(mk='a9b403e150dd4af8953a52a4bb841051e4b705d9')
2545+
indv2 = Individual.objects.create(mk='185c4b709e5446d250b4fde0e34b78a2b4fde0e3')
2546+
indv3 = Individual.objects.create(mk='c6d2504fde0e34b78a185c4b709e5442d045451c')
2547+
Profile.objects.create(name='AA', individual=indv1)
2548+
Profile.objects.create(name='ZZ', individual=indv2)
2549+
Profile.objects.create(name='MM', individual=indv3)
2550+
2551+
# Test ascending order
2552+
client = graphene.test.Client(schema)
2553+
executed = client.execute(SH_INDIVIDUALS_ORDER_BY % 'profile__name',
2554+
context_value=self.context_value)
2555+
2556+
individuals = executed['data']['individuals']['entities']
2557+
2558+
indv = individuals[0]
2559+
self.assertEqual(indv['mk'], indv1.mk)
2560+
indv = individuals[1]
2561+
self.assertEqual(indv['mk'], indv3.mk)
2562+
indv = individuals[2]
2563+
self.assertEqual(indv['mk'], indv2.mk)
2564+
2565+
# Test descending order
2566+
executed = client.execute(SH_INDIVIDUALS_ORDER_BY % '-profile__name',
2567+
context_value=self.context_value)
2568+
2569+
individuals = executed['data']['individuals']['entities']
2570+
2571+
indv = individuals[0]
2572+
self.assertEqual(indv['mk'], indv2.mk)
2573+
indv = individuals[1]
2574+
self.assertEqual(indv['mk'], indv3.mk)
2575+
indv = individuals[2]
2576+
self.assertEqual(indv['mk'], indv1.mk)
2577+
25412578
def test_pagination(self):
25422579
"""Check whether it returns the individuals searched when using pagination"""
25432580

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import DateInput from "./DateInput.vue";
2+
3+
export default {
4+
title: "DateInput",
5+
excludeStories: /.*Data$/
6+
};
7+
8+
const DateInputTemplate = `
9+
<div class="ma-5 col-3">
10+
<date-input v-model="date" label="Label" :filled="filled" :outlined="outlined" />
11+
</div>`;
12+
13+
export const Default = () => ({
14+
components: { DateInput },
15+
template: DateInputTemplate,
16+
data() {
17+
return {
18+
date: null,
19+
filled: false,
20+
outlined: false
21+
}
22+
}
23+
});
24+
25+
export const Filled = () => ({
26+
components: { DateInput },
27+
template: DateInputTemplate,
28+
data() {
29+
return {
30+
date: null,
31+
filled: true,
32+
outlined: false
33+
}
34+
}
35+
});
36+
37+
export const Outlined = () => ({
38+
components: { DateInput },
39+
template: DateInputTemplate,
40+
data() {
41+
return {
42+
date: null,
43+
filled: false,
44+
outlined: true
45+
}
46+
}
47+
});
48+
49+
export const Error = () => ({
50+
components: { DateInput },
51+
template: DateInputTemplate,
52+
data() {
53+
return {
54+
date: "abc",
55+
filled: false,
56+
outlined: true
57+
}
58+
}
59+
});

ui/src/components/DateInput.vue

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<template>
2+
<v-menu
3+
v-model="openPicker"
4+
:close-on-content-click="false"
5+
transition="scale-transition"
6+
min-width="290px"
7+
nudge-bottom="40"
8+
right
9+
>
10+
<template v-slot:activator="{ on }">
11+
<v-text-field
12+
v-model="inputDate"
13+
:label="label"
14+
:filled="filled"
15+
:dense="outlined"
16+
:outlined="outlined"
17+
:single-line="outlined"
18+
:error-messages="error"
19+
height="30"
20+
clearable
21+
hide-details="auto"
22+
v-on="on"
23+
@change="setInputDate($event)"
24+
></v-text-field>
25+
</template>
26+
<v-date-picker v-model="date" :min="min" :max="max" no-title scrollable>
27+
</v-date-picker>
28+
</v-menu>
29+
</template>
30+
31+
<script>
32+
export default {
33+
name: "DateInput",
34+
props: {
35+
value: {
36+
type: [String, Date]
37+
},
38+
label: {
39+
type: String,
40+
required: true
41+
},
42+
min: {
43+
type: [String, Date],
44+
required: false
45+
},
46+
max: {
47+
type: [String, Date],
48+
required: false
49+
},
50+
filled: {
51+
type: Boolean,
52+
required: false,
53+
default: false
54+
},
55+
outlined: {
56+
type: Boolean,
57+
required: false,
58+
default: false
59+
}
60+
},
61+
data() {
62+
return {
63+
openPicker: false,
64+
error: null,
65+
inputDate: this.value
66+
};
67+
},
68+
computed: {
69+
date: {
70+
get() {
71+
try {
72+
const ISODate = this.value
73+
? new Date(this.value).toISOString()
74+
: null;
75+
this.setInputDate(ISODate);
76+
return ISODate;
77+
} catch {
78+
this.setError("Invalid date");
79+
return null;
80+
}
81+
},
82+
set(value) {
83+
const ISODate = new Date(value).toISOString();
84+
this.$emit("input", ISODate);
85+
this.openPicker = false;
86+
this.setInputDate(ISODate);
87+
this.setError(null);
88+
}
89+
}
90+
},
91+
methods: {
92+
setError(error) {
93+
this.error = error;
94+
},
95+
setInputDate(date) {
96+
if (!date) {
97+
return;
98+
}
99+
try {
100+
const ISODate = date ? new Date(date).toISOString() : null;
101+
this.$emit("input", ISODate);
102+
this.setError(null);
103+
this.openPicker = false;
104+
this.inputDate = ISODate.substring(0, 10);
105+
} catch {
106+
this.setError("Invalid date");
107+
}
108+
}
109+
}
110+
};
111+
</script>

0 commit comments

Comments
 (0)