Commit ccc3f20c3c894f204e8c68e6e5ddd4cddbb913ff

Add support for showing paragraph authors in a document

This feature is enabled with the document setting:

show_authors: true

Additionally, new classes can be forced to a paragraph that is created via clicking a button with ".addAbove" class. This is achieved by adding forceclass="classname" to the paragraph containing the button.
timApp/common.py
(4 / 0)
  
6464 # Skip readings and notes
6565 return process_areas(html_pars, macros, delimiter, env), js_paths, css_paths, modules
6666
67 if doc.get_settings().show_authors():
68 authors = doc.get_changelog().get_authorinfo(pars)
69 for p in html_pars:
70 p['authorinfo'] = authors.get(p['id'])
6771 # There can be several references of the same paragraph in the document, which is why we need a dict of lists
6872 pars_dict = defaultdict(list)
6973
timApp/documentmodel/changelog.py
(70 / 0)
  
1from collections import defaultdict
2from typing import List, Dict, Union, Tuple, Optional
3
4import timApp.timdb.models
5from timApp.documentmodel.changelogentry import ChangelogEntry
6from timApp.documentmodel.docparagraph import DocParagraph
7from timApp.timdb.tim_models import db
8
9UserOrGroup = Union['timApp.timdb.models.user.User', 'timApp.timdb.models.usergroup.UserGroup']
10
11
12def get_author_str(u: UserOrGroup, es: List[ChangelogEntry]):
13 display_name = u.pretty_full_name
14 num_changes = len(es)
15 return display_name if num_changes <= 1 else f'{display_name} ({num_changes} edits)'
16
17
18class AuthorInfo:
19 def __init__(self,
20 user_map: Dict[int, UserOrGroup],
21 entries: Dict[int, List[ChangelogEntry]]) -> None:
22 self.authors: Dict[UserOrGroup, List[ChangelogEntry]] = {}
23 for k, v in entries.items():
24 self.authors[user_map[k]] = v
25
26 @property
27 def display_name(self):
28 return '; '.join(get_author_str(u, es) for u, es in self.authors.items())
29
30 @property
31 def time(self):
32 return max(entries[-1].time for entries in self.authors.values())
33
34
35class Changelog:
36 def __init__(self) -> None:
37 self.entries: List[ChangelogEntry] = []
38
39 def append(self, entry: ChangelogEntry):
40 self.entries.append(entry)
41
42 def to_json(self):
43 return self.entries
44
45 def get_authorinfo(self, pars: List[DocParagraph]) -> Dict[str, AuthorInfo]:
46 usergroup_ids = set()
47 par_ids = set(p.get_id() for p in pars)
48 par_author_map = {}
49 if not par_ids:
50 return par_author_map
51 par_entry_map: Dict[str, Dict[int, List[ChangelogEntry]]] = defaultdict(lambda: defaultdict(list))
52 ug_obj_map = {}
53 for e in self.entries:
54 if e.par_id in par_ids:
55 usergroup_ids.add(e.group_id)
56 par_entry_map[e.par_id][e.group_id].append(e)
57 User = timApp.timdb.models.user.User
58 UserGroup = timApp.timdb.models.usergroup.UserGroup
59 result = db.session.query(UserGroup, User).filter(
60 UserGroup.id.in_(usergroup_ids)).outerjoin(User,
61 User.name == UserGroup.name).all() # type: List[Tuple[UserGroup, Optional[User]]]
62 for ug, u in result:
63 ug_obj_map[ug.id] = u or ug
64 for i in par_ids:
65 entry = par_entry_map.get(i)
66 if not entry:
67 continue
68 par_author_map[i] = AuthorInfo(ug_obj_map, par_entry_map[i])
69
70 return par_author_map
timApp/documentmodel/changelogentry.py
(85 / 0)
  
1from datetime import timezone
2from enum import Enum
3
4import dateutil
5import dateutil.parser
6
7from timApp.documentmodel.version import Version
8from timApp.timdb.models.usergroup import UserGroup
9
10
11class OperationType(Enum):
12 Add = 'Added'
13 Delete = 'Deleted'
14 Insert = 'Inserted'
15 Modify = 'Modified'
16
17
18class Operation:
19 def __init__(self, op: OperationType) -> None:
20 self.op = op
21
22 @staticmethod
23 def from_type_dict(op: OperationType, d: dict) -> 'Operation':
24 if op == OperationType.Add:
25 return AddOperation(op)
26 elif op == OperationType.Delete:
27 return DeleteOperation(op)
28 elif op == OperationType.Insert:
29 return InsertOperation(op, d['before_id'])
30 elif op == OperationType.Modify:
31 return ModifyOperation(op, old_hash=d['old_hash'], new_hash=d['new_hash'])
32 else:
33 assert False, 'Unknown OperationType'
34
35 def to_json(self):
36 return None
37
38
39class ModifyOperation(Operation):
40 def to_json(self):
41 return {'old_hash': self.old_hash, 'new_hash': self.new_hash}
42
43 def __init__(self, op: OperationType, old_hash: str, new_hash: str) -> None:
44 super().__init__(op)
45 self.old_hash = old_hash
46 self.new_hash = new_hash
47
48
49class DeleteOperation(Operation):
50 def __init__(self, op: OperationType) -> None:
51 super().__init__(op)
52
53
54class AddOperation(Operation):
55 def __init__(self, op: OperationType) -> None:
56 super().__init__(op)
57
58
59class InsertOperation(Operation):
60 def to_json(self):
61 return {'before_id': self.before_id}
62
63 def __init__(self, op: OperationType, before_id: str) -> None:
64 super().__init__(op)
65 self.before_id = before_id
66
67
68class ChangelogEntry:
69 def __init__(self, par_id: str, ver: Version, op: str, time: str, group_id: int,
70 op_params: dict):
71 self.version = ver
72 self.time = dateutil.parser.parse(time).replace(tzinfo=timezone.utc)
73 self.op = Operation.from_type_dict(OperationType(op), op_params)
74 self.group_id = group_id
75 self.par_id = par_id
76
77 def to_json(self):
78 return {
79 'ver': self.version,
80 'time': self.time,
81 'op': self.op.op.value,
82 'group': UserGroup.query.get(self.group_id).name,
83 'par_id': self.par_id,
84 'op_params': self.op.to_json()
85 }
timApp/documentmodel/docsettings.py
(4 / 0)
  
3333 plugin_md_key = 'plugin_md'
3434 print_settings_key = 'print_settings'
3535 preamble_key = 'preamble'
36 show_authors_key = 'show_authors'
3637
3738 @staticmethod
3839 def settings_to_string(par: DocParagraph) -> str:
207207 def is_texplain(self):
208208 texplain = self.__dict.get('texplain', False)
209209 return texplain
210
211 def show_authors(self, default=False):
212 return self.__dict.get(self.show_authors_key, default)
210213
211214
212215def resolve_settings_for_pars(pars: Iterable[DocParagraph]) -> YamlBlock:
timApp/documentmodel/document.py
(12 / 14)
  
1212from flask import g
1313from lxml import etree, html
1414
15from timApp.documentmodel.changelog import Changelog
16from timApp.documentmodel.changelogentry import ChangelogEntry
1517from timApp.documentmodel.docparagraph import DocParagraph
1618from timApp.documentmodel.docsettings import DocSettings, resolve_settings_for_pars
1719from timApp.documentmodel.documenteditresult import DocumentEditResult
2323from timApp.documentmodel.exceptions import DocExistsError, ValidationException
2424from timApp.documentmodel.preloadoption import PreloadOption
2525from timApp.documentmodel.validationresult import ValidationResult
26from timApp.documentmodel.version import Version
2627from timApp.documentmodel.yamlblock import YamlBlock
2728from timApp.timdb.invalidreferenceexception import InvalidReferenceException
2829from timApp.timdb.timdbexception import TimDbException, PreambleException
123123 return os.path.exists(os.path.join(cls.get_documents_dir(froot), str(doc_id)))
124124
125125 @classmethod
126 def version_exists(cls, doc_id: int, doc_ver: Tuple[int, int], files_root: Optional[str] = None) -> bool:
126 def version_exists(cls, doc_id: int, doc_ver: Version, files_root: Optional[str] = None) -> bool:
127127 """Checks if a document version exists.
128128
129129 :param doc_id: Document id.
350350 res = 1 + cls.__get_largest_file_number(cls.get_documents_dir(froot), default=0)
351351 return res
352352
353 def get_version(self) -> Tuple[int, int]:
353 def get_version(self) -> Version:
354354 """Gets the latest version of the document as a major-minor tuple.
355355
356356 :return: Latest version, or (-1, 0) if there isn't yet one.
392392 def getlogfilename(self) -> str:
393393 return os.path.join(self.get_document_path(), 'changelog')
394394
395 def __write_changelog(self, ver: Tuple[int, int], operation: str, par_id: str, op_params: Optional[dict] = None):
395 def __write_changelog(self, ver: Version, operation: str, par_id: str, op_params: Optional[dict] = None):
396396 logname = self.getlogfilename()
397397 src = open(logname, 'r') if os.path.exists(logname) else None
398398 destfd, tmpname = mkstemp()
424424 os.unlink(tmpname)
425425
426426 def __increment_version(self, op: str, par_id: str, increment_major: bool,
427 op_params: Optional[dict] = None) -> Tuple[int, int]:
427 op_params: Optional[dict] = None) -> Version:
428428 ver_exists = True
429429 ver = self.get_version()
430430 old_ver = None
452452 self.ref_doc_cache = {}
453453 return ver
454454
455 def __update_metadata(self, pars: List[DocParagraph], old_ver: Tuple[int, int], new_ver: Tuple[int, int]):
455 def __update_metadata(self, pars: List[DocParagraph], old_ver: Version, new_ver: Version):
456456 if old_ver == new_ver:
457457 raise TimDbException("__update_metadata called with old_ver == new_ver")
458458 new_reflist_file = self.get_reflist_filename(new_ver)
852852 current_headers[1].append(current)
853853 return current_headers
854854
855 def get_changelog(self, max_entries: int = 100) -> List[dict]:
856 log = []
855 def get_changelog(self, max_entries: int = 100) -> Changelog:
856 log = Changelog()
857857 logname = self.getlogfilename()
858858 if not os.path.isfile(logname):
859 return []
859 return Changelog()
860860
861861 lc = max_entries
862862 with open(logname, 'r') as f:
866866 break
867867 try:
868868 entry = json.loads(line)
869 entry['time'] = dateutil.parser.parse(entry['time']).replace(tzinfo=timezone.utc)
870 log.append(entry)
869 log.append(ChangelogEntry(**entry))
871870 except ValueError:
872871 print(f"doc id {self.doc_id}: malformed log line: {line}")
873872 lc -= 1
888888 if rp.get_attr('taskId') == task_id_name:
889889 return rp
890890 raise TimDbException(f'Task not found in the document: {task_id_name}')
891
892 def get_last_modified(self) -> Optional[datetime]:
893 log = self.get_changelog(max_entries=1)
894 return log[0]['time'] if log is not None and len(log) > 0 else None
895891
896892 def delete_section(self, area_start, area_end) -> DocumentEditResult:
897893 result = DocumentEditResult()
timApp/documentmodel/documentversion.py
(3 / 2)
  
55from timApp.documentmodel.document import Document
66from timApp.documentmodel.docparagraph import DocParagraph
77from timApp.documentmodel.preloadoption import PreloadOption
8from timApp.documentmodel.version import Version
89
910
1011class DocumentVersion(Document):
1112
12 def __init__(self, doc_id: int, doc_ver: Tuple[int, int],
13 def __init__(self, doc_id: int, doc_ver: Version,
1314 files_root=None, modifier_group_id: Optional[int] = 0, preload_option = PreloadOption.none):
1415 super(DocumentVersion, self).__init__(doc_id, files_root, modifier_group_id, preload_option)
1516 self.does_exist = None
5050 def remove(cls, doc_id: int, files_root: Optional[str] = None, ignore_exists=False):
5151 assert False, "Called DocumentVersion.remove"
5252
53 def get_version(self) -> Tuple[int, int]:
53 def get_version(self) -> Version:
5454 return self.version
5555
5656 def has_paragraph(self, par_id: str) -> bool:
timApp/documentmodel/timjsonencoder.py
(7 / 2)
  
22import json
33
44from isodate import duration_isoformat
5from jinja2 import Undefined
56from sqlalchemy.ext.declarative import DeclarativeMeta
67
78
1818 if isinstance(o, datetime.timedelta):
1919 return duration_isoformat(o)
2020
21 if isinstance(o, Undefined):
22 return None
23
24 tojson = getattr(o, 'to_json', None)
25 if tojson:
26 return tojson()
2127 # from http://stackoverflow.com/a/31569287 with some changes
2228 if isinstance(o.__class__, DeclarativeMeta):
23 if hasattr(o, 'to_json'):
24 return o.to_json()
2529 data = {}
2630 fields = o.__json__() if hasattr(o, '__json__') else dir(o)
2731 for field in [f for f in fields if not f.startswith('_') and f not in ['metadata', 'query', 'query_class']]:
timApp/documentmodel/version.py
(11 / 0)
  
1from typing import Tuple
2
3"""Document version number.
4
5Format is major, minor.
6
7Major version is incremented whenever a paragraph is added, inserted or deleted from the document. The minor version is reset to
8zero.
9Minor version is incremented whenever a paragraph is modified.
10"""
11Version = Tuple[int, int]
timApp/routes/edit.py
(2 / 3)
  
1414from timApp.documentmodel.documenteditresult import DocumentEditResult
1515from timApp.documentmodel.exceptions import ValidationException, ValidationWarning
1616from timApp.documentmodel.preloadoption import PreloadOption
17from timApp.documentmodel.version import Version
1718from timApp.markdownconverter import md_to_html
1819from timApp.requesthelper import verify_json_params
1920from timApp.responsehelper import json_response, ok_response
108108 return manage_response(docentry, pars, timdb, ver_before)
109109
110110
111def manage_response(docentry: DocInfo, pars: List[DocParagraph], timdb, ver_before: Tuple[int, int]):
111def manage_response(docentry: DocInfo, pars: List[DocParagraph], timdb, ver_before: Version):
112112 doc = docentry.document_as_current_user
113113 chg = doc.get_changelog()
114 for ver in chg:
115 ver['group'] = timdb.users.get_user_group_name(ver.pop('group_id'))
116114 notify_doc_watchers(docentry,
117115 get_diff_link(docentry, ver_before),
118116 NotificationType.DocModified)
timApp/routes/editrequest.py
(10 / 4)
  
1010
1111class EditRequest:
1212 def __init__(self, doc: Document, area_start: str = None, area_end: str = None, par: str = None, text: str = None,
13 next_par_id: str = None, preview: bool = False):
13 next_par_id: str = None, preview: bool = False, forced_classes: Optional[List[str]]=None):
14 self.forced_classes = forced_classes or []
1415 self.doc = doc
1516 self.preview = preview
1617 self.old_doc_version = doc.get_version()
5959 if self.editor_pars is None:
6060 self.editor_pars = get_pars_from_editor_text(self.doc, self.text, break_on_elements=self.editing_area,
6161 skip_access_check=skip_access_check)
62 for c in self.forced_classes:
63 for p in self.editor_pars:
64 p.add_class(c)
6265 return self.editor_pars
6366
6467 @staticmethod
6568 def from_request(doc: Document, text: Optional[str] = None, preview: bool = False) -> 'EditRequest':
6669 if text is None:
6770 text, = verify_json_params('text')
68 area_start, area_end, par, par_next = verify_json_params('area_start', 'area_end', 'par', 'par_next',
69 require=False)
71 area_start, area_end, par, par_next, forced_classes = verify_json_params('area_start', 'area_end', 'par',
72 'par_next', 'forced_classes',
73 require=False)
7074 return EditRequest(doc=doc,
7175 text=text,
7276 area_start=area_start,
7377 area_end=area_end,
7478 par=par,
7579 next_par_id=par_next,
76 preview=preview)
80 preview=preview,
81 forced_classes=forced_classes)
7782
7883
7984def get_pars_from_editor_text(doc: Document, text: str,
timApp/static/scripts/tim/controllers/view/editing.ts
(28 / 30)
  
1010import {onClick} from "./eventhandlers";
1111import {ParCompiler} from "../../services/parCompiler";
1212
13function prepareOptions($this: Element, saveTag: string): [JQuery, {}] {
14 $(".actionButtons").remove();
15 // var $par = $('.par').last();
16 // return sc.showAddParagraphBelow(e, $par);
17 // return sc.showAddParagraphAbove(e, sc.$pars);
18 var par = $($this).closest('.par');
19 var text = par.find('pre').text();
20 // text = text.replace('⁞', ''); // TODO: set cursor to | position
21 let forcedClasses = [];
22 const forceAttr = getParAttributes(par).forceclass;
23 if (forceAttr) {
24 forcedClasses = forceAttr.split(" ");
25 }
26 var options = {
27 'localSaveTag': saveTag,
28 'texts': {
29 'beforeText': "alkuun",
30 'initialText': text,
31 'afterText': "loppuun",
32 },
33 forcedClasses: forcedClasses,
34 };
35 return [par, options];
36}
37
1338// Wrap given text to max n chars length lines spliting from space
1439export function wrapText(s, n)
1540{
131131 par_next: parNextId, // the id of the paragraph that follows par or null if par is the last one
132132 area_start: areaStart,
133133 area_end: areaEnd,
134 forced_classes: options.forcedClasses,
134135 tags,
135136 },
136137 "options": {
452452 });
453453
454454 onClick(".addAbove", function($this, e) {
455 $(".actionButtons").remove();
456 // var $par = $('.par').last();
457 // return sc.showAddParagraphBelow(e, $par);
458 // return sc.showAddParagraphAbove(e, sc.$pars);
459 var par = $($this).closest('.par');
460 var text = par.find('pre').text();
461 // text = text.replace('⁞', ''); // TODO: set cursor to | position
462 var options = {
463 'localSaveTag': 'addAbove',
464 'texts': {
465 'beforeText': "alkuun",
466 'initialText': text ,
467 'afterText': "loppuun"
468 }
469 }
455 const [par, options] = prepareOptions($this, "addAbove");
470456 return sc.showAddParagraphAbove(e, par, options);
471457 });
472458
473459 onClick(".addBelow", function($this, e) {
474 $(".actionButtons").remove();
475 // var $par = $('.par').last();
476 // return sc.showAddParagraphBelow(e, $par);
477 // return sc.showAddParagraphAbove(e, sc.$pars);
478 var par = $($this).closest('.par');
479 var text = par.find('pre').text();
480 // text = text.replace('⁞', ''); // TODO: set cursor to | position
481 var options = {
482 'localSaveTag': 'addBelow',
483 'texts': {
484 'beforeText': "alkuun",
485 'initialText': text ,
486 'afterText': "loppuun"
487 }
488 }
460 const [par, options] = prepareOptions($this, "addBelow");
489461 return sc.showAddParagraphBelow(e, par, options);
490462 });
491463
timApp/static/stylesheet.scss
(10 / 0)
  
110110 .linkit > .parContent {
111111 font-size: small;
112112 }
113 .authorinfo {
114 top: -1.1em;
115 position: relative;
116 font-size: x-small;
117 height: 0.5em;
118
119 .timestamp {
120 color: lightgrey;
121 }
122 }
113123 .parContent {
114124 text-align: $body-text-align;
115125 @media (max-width: $screen-xs-max) {
timApp/templates/partials/paragraphs.html
(7 / 0)
  
147147 <div class="parContent"{% if task_id %} id="{{ task_id }}"{% endif %}>
148148 {{ t.html|safe }}
149149 </div>
150 {%- if t.authorinfo -%}
151 <div class="authorinfo">
152 <i class="glyphicon glyphicon-pencil"></i>
153 <span class="username">{{t.authorinfo.display_name}}</span>
154 <span class="timestamp">{{ t.authorinfo.time|datestr_to_relative }}</span>
155 </div>
156 {%- endif -%}
150157 {%- endif -%}
151158
152159 {%- if t.attrs.rl == 'force' or not (not t.attrs.rd or t.attrs.rl == 'no') -%}
timApp/tests/db/test_document.py
(15 / 15)
  
2727 self.assertTrue(d.exists())
2828 self.assertEqual(d.doc_id + 1, Document.get_next_free_id())
2929 self.assertEqual((0, 0), d.get_version())
30 self.assertListEqual([], d.get_changelog())
30 self.assertListEqual([], d.get_changelog().entries)
3131
3232 d = self.create_doc().document
3333 self.assertTrue(d.exists())
3434 self.assertEqual(d.doc_id + 1, Document.get_next_free_id())
3535 self.assertEqual((0, 0), d.get_version())
36 self.assertListEqual([], d.get_changelog())
36 self.assertListEqual([], d.get_changelog().entries)
3737
3838 with self.assertRaises(DocExistsError):
3939 d.create()
4747 self.assertTrue(d.has_paragraph(par1.get_id()))
4848 self.assertFalse(d.has_paragraph(par1.get_id()[:-1]))
4949 self.assertEqual((1, 0), d.get_version())
50 self.assertEqual(1, len(d.get_changelog()))
50 self.assertEqual(1, len(d.get_changelog().entries))
5151
5252 # Add different next paragraph
5353 par2 = d.add_paragraph('different')
5454 self.assertEqual('different', par2.get_markdown())
5555 self.assertTrue(d.has_paragraph(par2.get_id()))
5656 self.assertEqual((2, 0), d.get_version())
57 self.assertEqual(2, len(d.get_changelog()))
57 self.assertEqual(2, len(d.get_changelog().entries))
5858 self.assertNotEqual(par1.get_id(), par2.get_id())
5959
6060 # Add next paragraph with same text as the first
6262 self.assertEqual('testing', par3.get_markdown())
6363 self.assertTrue(d.has_paragraph(par3.get_id()))
6464 self.assertEqual((3, 0), d.get_version())
65 self.assertEqual(3, len(d.get_changelog()))
65 self.assertEqual(3, len(d.get_changelog().entries))
6666 self.assertNotEqual(par1.get_id(), par2.get_id())
6767
6868 # Add an empty paragraph
7070 self.assertEqual('', par3.get_markdown())
7171 self.assertTrue(d.has_paragraph(par3.get_id()))
7272 self.assertEqual((4, 0), d.get_version())
73 self.assertEqual(4, len(d.get_changelog()))
73 self.assertEqual(4, len(d.get_changelog().entries))
7474 self.assertNotEqual(par2.get_id(), par3.get_id())
7575 self.assertNotEqual(par1.get_id(), par3.get_id())
7676
7979
8080 pars = [d.add_paragraph(random_paragraph()) for _ in range(0, 10)]
8181 self.assertEqual((10, 0), d.get_version())
82 self.assertEqual(10, len(d.get_changelog()))
82 self.assertEqual(10, len(d.get_changelog().entries))
8383 self.assertListEqual([p.get_id() for p in pars], [par.get_id() for par in d])
8484 self.assertListEqual([p.get_hash() for p in pars], [par.get_hash() for par in d])
8585
9595 pars.remove(pars[0])
9696 self.assertListEqual(pars, [par.get_id() for par in d])
9797 self.assertEqual((11, 0), d.get_version())
98 self.assertEqual(11, len(d.get_changelog()))
98 self.assertEqual(11, len(d.get_changelog().entries))
9999
100100 # Delete from the middle
101101 d.delete_paragraph(pars[2])
105105 pars.remove(pars[2])
106106 self.assertListEqual(pars, [par.get_id() for par in d])
107107 self.assertEqual((12, 0), d.get_version())
108 self.assertEqual(12, len(d.get_changelog()))
108 self.assertEqual(12, len(d.get_changelog().entries))
109109
110110 # Delete last paragraph
111111 n = len(pars)
116116 pars.remove(pars[n - 1])
117117 self.assertListEqual(pars, [par.get_id() for par in d])
118118 self.assertEqual((13, 0), d.get_version())
119 self.assertEqual(13, len(d.get_changelog()))
119 self.assertEqual(13, len(d.get_changelog().entries))
120120
121121 def test_insertparagraph(self):
122122 d = self.create_doc().document
127127 pars = [par.get_id()] + pars
128128 self.assertListEqual(pars, [par.get_id() for par in d])
129129 self.assertEqual((11, 0), d.get_version())
130 self.assertEqual(11, len(d.get_changelog()))
130 self.assertEqual(11, len(d.get_changelog().entries))
131131
132132 # Insert in the middle
133133 par = d.insert_paragraph('middle', insert_before_id=pars[4])
134134 pars = pars[0:4] + [par.get_id()] + pars[4:]
135135 self.assertListEqual(pars, [par.get_id() for par in d])
136136 self.assertEqual((12, 0), d.get_version())
137 self.assertEqual(12, len(d.get_changelog()))
137 self.assertEqual(12, len(d.get_changelog().entries))
138138
139139 # Insert as last
140140 par = d.insert_paragraph('last', insert_before_id=None)
141141 pars.append(par.get_id())
142142 self.assertListEqual(pars, [par.get_id() for par in d])
143143 self.assertEqual((13, 0), d.get_version())
144 self.assertEqual(13, len(d.get_changelog()))
144 self.assertEqual(13, len(d.get_changelog().entries))
145145
146146 def test_get_html(self):
147147 d = self.create_doc().document
168168 self.assertEqual(new_text, par2_mod.get_markdown())
169169 self.assertNotEqual(par2_hash, par2_mod.get_hash())
170170 self.assertEqual((10, 1), d.get_version())
171 self.assertEqual(11, len(d.get_changelog()))
171 self.assertEqual(11, len(d.get_changelog().entries))
172172
173173 par2_mod = d.modify_paragraph(par2_id, old_md)
174174 self.assertEqual(old_md, par2_mod.get_markdown())
183183 self.assertEqual(new_text, par2_mod.get_markdown())
184184 self.assertNotEqual(par2_hash, par2_mod.get_hash())
185185 self.assertEqual((10, i + 3), d.get_version())
186 self.assertEqual(13 + i, len(d.get_changelog()))
186 self.assertEqual(13 + i, len(d.get_changelog().entries))
187187
188188 def test_document_remove(self):
189189 free = Document.get_next_free_id()
timApp/tests/server/test_authors.py
(40 / 0)
  
1"""Server tests for showing authors for paragraphs."""
2from timApp.tests.server.timroutetest import TimRouteTest, get_content
3from timApp.timdb.userutils import grant_edit_access
4
5
6class AuthorsTest(TimRouteTest):
7 def test_authors(self):
8 self.login_test1()
9 d = self.create_doc()
10 url = d.url
11 grant_edit_access(self.test_user_2.get_personal_group().id, d.id)
12 grant_edit_access(self.test_user_3.get_personal_group().id, d.id)
13 self.new_par(d.document, 'par 1')
14 self.new_par(d.document, 'par 2')
15 self.new_par(d.document, 'par 3')
16 d.document.set_settings({'show_authors': True})
17 pars = d.document.get_paragraphs()
18 username_selector = '.authorinfo .username'
19 authors = get_content(self.get(url, as_tree=True), username_selector)
20 self.assertEqual(authors, ['Logged-in users', 'Test user 1', 'Test user 1', 'Test user 1'])
21 self.post_par(d.document, 'edit', pars[1].get_id())
22 authors = get_content(self.get(url, as_tree=True), username_selector)
23 self.assertEqual(authors, ['Logged-in users', 'Test user 1 (2 edits)', 'Test user 1', 'Test user 1'])
24 self.login_test2()
25 self.post_par(d.document, 'edit2', pars[1].get_id())
26 authors = get_content(self.get(url, as_tree=True), username_selector)
27 self.assertEqual(authors, ['Logged-in users',
28 'Test user 2; Test user 1 (2 edits)',
29 'Test user 1',
30 'Test user 1'])
31 self.post_par(d.document, 'edit3', pars[1].get_id())
32 self.post_par(d.document, 'edit3', pars[3].get_id())
33 authors = get_content(self.get(url, as_tree=True), username_selector)
34 self.assertEqual(authors, ['Logged-in users',
35 'Test user 2 (2 edits); Test user 1 (2 edits)',
36 'Test user 1',
37 'Test user 2; Test user 1'])
38 d.document.set_settings({})
39 authors = get_content(self.get(url, as_tree=True), username_selector)
40 self.assertEqual(authors, [])
timApp/tests/server/test_notify.py
(5 / 5)
  
5656 'mail_from': mail_from,
5757 'msg': f'Link to the paragraph: {url}#{pars[2].get_id()}\n'
5858 '\n'
59 'Link to changes: http://localhost/diff/5/2/1/3/0\n'
59 f'Link to changes: http://localhost/diff/{d.id}/2/1/3/0\n'
6060 '\n'
6161 'Paragraph was added:\n'
6262 '\n'
7373 'mail_from': mail_from,
7474 'msg': f'Link to the paragraph: {url}#{pars[1].get_id()}\n'
7575 '\n'
76 'Link to changes: http://localhost/diff/5/3/0/3/1\n'
76 f'Link to changes: http://localhost/diff/{d.id}/3/0/3/1\n'
7777 '\n'
7878 'Paragraph was edited:\n'
7979 '\n'
8888 'mail_from': mail_from,
8989 'msg': f'Link to the document: {url}\n'
9090 '\n'
91 'Link to changes: http://localhost/diff/5/3/1/4/0\n'
91 f'Link to changes: http://localhost/diff/{d.id}/3/1/4/0\n'
9292 '\n'
9393 'Paragraph was deleted:\n'
9494 '\n'
105105 'mail_from': mail_from,
106106 'msg': f'Link to the paragraph: {url}#{pars[-1].get_id()}\n'
107107 '\n'
108 'Link to changes: http://localhost/diff/5/4/0/5/0\n'
108 f'Link to changes: http://localhost/diff/{d.id}/4/0/5/0\n'
109109 '\n'
110110 'Paragraph was added:\n'
111111 '\n'
112112 f'{pars[-1].get_markdown()}',
113113 'rcpt': self.test_user_2.email,
114114 'reply_to': self.test_user_1.email,
115 'subject': f'user Test edited the document {title}'}, sent_mails_in_testing[-1])
115 'subject': f'Test user 1 edited the document {title}'}, sent_mails_in_testing[-1])
116116
117117 def test_revoke_view_no_email(self):
118118 d, title, url = self.prepare_doc()
timApp/tests/server/timroutetest.py
(2 / 2)
  
5151testclient = testclient.__enter__()
5252
5353
54def get_content(element: HtmlElement) -> List[str]:
55 return [r.text_content().strip() for r in element.cssselect('.parContent')]
54def get_content(element: HtmlElement, selector: str='.parContent') -> List[str]:
55 return [r.text_content().strip() for r in element.cssselect(selector)]
5656
5757
5858class TimRouteTest(TimDbTest):
timApp/timdb/__init__.py
(0 / 1)
  
1# Init file for python
timApp/timdb/docinfo.py
(1 / 4)
  
122122 def get_changelog_with_names(self, length=None):
123123 if not length:
124124 length = getattr(self, 'changelog_length', 100)
125 changelog = self.document.get_changelog(length)
126 for ver in changelog:
127 ver['group'] = UserGroup.query.get(ver.pop('group_id')).name
128 return changelog
125 return self.document.get_changelog(length)
129126
130127 def get_notifications(self, notify_type: NotificationType) -> List[Notification]:
131128 q = Notification.query.filter_by(doc_id=self.id)
timApp/timdb/models/user.py
(1 / 1)
  
5151 @property
5252 def is_email_user(self):
5353 """Returns whether the user signed up via email."""
54 return '@' in self.name
54 return '@' in self.name or self.name.startswith('testuser')
5555
5656 @property
5757 def pretty_full_name(self):
timApp/timdb/models/usergroup.py
(4 / 0)
  
2323 def __json__(self) -> List[str]:
2424 return ['id', 'name']
2525
26 @property
27 def pretty_full_name(self):
28 return self.name
29
2630 @staticmethod
2731 def create(name: str, commit: bool = True) -> 'UserGroup':
2832 """Creates a new user group.